layout-main.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. 'use client'
  2. import type { FC, SVGProps } from 'react'
  3. import React, { useEffect, useMemo } from 'react'
  4. import { usePathname } from 'next/navigation'
  5. import useSWR from 'swr'
  6. import { useTranslation } from 'react-i18next'
  7. import { useBoolean } from 'ahooks'
  8. import {
  9. Cog8ToothIcon,
  10. DocumentTextIcon,
  11. PaperClipIcon,
  12. } from '@heroicons/react/24/outline'
  13. import {
  14. Cog8ToothIcon as Cog8ToothSolidIcon,
  15. // CommandLineIcon as CommandLineSolidIcon,
  16. DocumentTextIcon as DocumentTextSolidIcon,
  17. } from '@heroicons/react/24/solid'
  18. import { RiApps2AddLine, RiBookOpenLine, RiInformation2Line } from '@remixicon/react'
  19. import s from './style.module.css'
  20. import classNames from '@/utils/classnames'
  21. import { fetchDatasetDetail, fetchDatasetRelatedApps } from '@/service/datasets'
  22. import type { RelatedAppResponse } from '@/models/datasets'
  23. import AppSideBar from '@/app/components/app-sidebar'
  24. import Loading from '@/app/components/base/loading'
  25. import DatasetDetailContext from '@/context/dataset-detail'
  26. import { DataSourceType } from '@/models/datasets'
  27. import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
  28. import { LanguagesSupported } from '@/i18n/language'
  29. import { useStore } from '@/app/components/app/store'
  30. import { getLocaleOnClient } from '@/i18n'
  31. import { useAppContext } from '@/context/app-context'
  32. import Tooltip from '@/app/components/base/tooltip'
  33. import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
  34. import { GetDatasetAuth } from '@/app/(commonLayout)/datasets/Container'
  35. export type IAppDetailLayoutProps = {
  36. children: React.ReactNode
  37. params: { datasetId: string }
  38. }
  39. const TargetIcon = ({ className }: SVGProps<SVGElement>) => {
  40. return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
  41. <g clipPath="url(#clip0_4610_6951)">
  42. <path d="M10.6666 5.33325V3.33325L12.6666 1.33325L13.3332 2.66659L14.6666 3.33325L12.6666 5.33325H10.6666ZM10.6666 5.33325L7.9999 7.99988M14.6666 7.99992C14.6666 11.6818 11.6818 14.6666 7.99992 14.6666C4.31802 14.6666 1.33325 11.6818 1.33325 7.99992C1.33325 4.31802 4.31802 1.33325 7.99992 1.33325M11.3333 7.99992C11.3333 9.84087 9.84087 11.3333 7.99992 11.3333C6.15897 11.3333 4.66659 9.84087 4.66659 7.99992C4.66659 6.15897 6.15897 4.66659 7.99992 4.66659" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
  43. </g>
  44. <defs>
  45. <clipPath id="clip0_4610_6951">
  46. <rect width="16" height="16" fill="white" />
  47. </clipPath>
  48. </defs>
  49. </svg>
  50. }
  51. const TargetSolidIcon = ({ className }: SVGProps<SVGElement>) => {
  52. return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
  53. <path fillRule="evenodd" clipRule="evenodd" d="M12.7733 0.67512C12.9848 0.709447 13.1669 0.843364 13.2627 1.03504L13.83 2.16961L14.9646 2.73689C15.1563 2.83273 15.2902 3.01486 15.3245 3.22639C15.3588 3.43792 15.2894 3.65305 15.1379 3.80458L13.1379 5.80458C13.0128 5.92961 12.8433 5.99985 12.6665 5.99985H10.9426L8.47124 8.47124C8.21089 8.73159 7.78878 8.73159 7.52843 8.47124C7.26808 8.21089 7.26808 7.78878 7.52843 7.52843L9.9998 5.05707V3.33318C9.9998 3.15637 10.07 2.9868 10.1951 2.86177L12.1951 0.861774C12.3466 0.710244 12.5617 0.640794 12.7733 0.67512Z" fill="#155EEF" />
  54. <path d="M1.99984 7.99984C1.99984 4.68613 4.68613 1.99984 7.99984 1.99984C8.36803 1.99984 8.6665 1.70136 8.6665 1.33317C8.6665 0.964981 8.36803 0.666504 7.99984 0.666504C3.94975 0.666504 0.666504 3.94975 0.666504 7.99984C0.666504 12.0499 3.94975 15.3332 7.99984 15.3332C12.0499 15.3332 15.3332 12.0499 15.3332 7.99984C15.3332 7.63165 15.0347 7.33317 14.6665 7.33317C14.2983 7.33317 13.9998 7.63165 13.9998 7.99984C13.9998 11.3135 11.3135 13.9998 7.99984 13.9998C4.68613 13.9998 1.99984 11.3135 1.99984 7.99984Z" fill="#155EEF" />
  55. <path d="M5.33317 7.99984C5.33317 6.52708 6.52708 5.33317 7.99984 5.33317C8.36803 5.33317 8.6665 5.03469 8.6665 4.6665C8.6665 4.29831 8.36803 3.99984 7.99984 3.99984C5.7907 3.99984 3.99984 5.7907 3.99984 7.99984C3.99984 10.209 5.7907 11.9998 7.99984 11.9998C10.209 11.9998 11.9998 10.209 11.9998 7.99984C11.9998 7.63165 11.7014 7.33317 11.3332 7.33317C10.965 7.33317 10.6665 7.63165 10.6665 7.99984C10.6665 9.4726 9.4726 10.6665 7.99984 10.6665C6.52708 10.6665 5.33317 9.4726 5.33317 7.99984Z" fill="#155EEF" />
  56. </svg>
  57. }
  58. type IExtraInfoProps = {
  59. isMobile: boolean
  60. relatedApps?: RelatedAppResponse
  61. expand: boolean
  62. }
  63. const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => {
  64. const locale = getLocaleOnClient()
  65. const [isShowTips, { toggle: toggleTips, set: setShowTips }] = useBoolean(!isMobile)
  66. const { t } = useTranslation()
  67. const hasRelatedApps = relatedApps?.data && relatedApps?.data?.length > 0
  68. const relatedAppsTotal = relatedApps?.data?.length || 0
  69. useEffect(() => {
  70. setShowTips(!isMobile)
  71. }, [isMobile, setShowTips])
  72. return <div>
  73. {hasRelatedApps && (
  74. <>
  75. {!isMobile && (
  76. <Tooltip
  77. position='right'
  78. noDecoration
  79. needsDelay
  80. popupContent={
  81. <LinkedAppsPanel
  82. relatedApps={relatedApps.data}
  83. isMobile={isMobile}
  84. />
  85. }
  86. >
  87. <div className='system-xs-medium-uppercase inline-flex cursor-pointer items-center space-x-1 text-text-secondary'>
  88. <span>{relatedAppsTotal || '--'} {t('common.datasetMenus.relatedApp')}</span>
  89. <RiInformation2Line className='h-4 w-4' />
  90. </div>
  91. </Tooltip>
  92. )}
  93. {isMobile && <div className={classNames(s.subTitle, 'flex items-center justify-center !px-0 gap-1')}>
  94. {relatedAppsTotal || '--'}
  95. <PaperClipIcon className='h-4 w-4 text-gray-700' />
  96. </div>}
  97. </>
  98. )}
  99. {!hasRelatedApps && !expand && (
  100. <Tooltip
  101. position='right'
  102. noDecoration
  103. needsDelay
  104. popupContent={
  105. <div className='w-[240px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-4'>
  106. <div className='inline-flex rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle p-2'>
  107. <RiApps2AddLine className='h-4 w-4 text-text-tertiary' />
  108. </div>
  109. <div className='my-2 text-xs text-text-tertiary'>{t('common.datasetMenus.emptyTip')}</div>
  110. <a
  111. className='mt-2 inline-flex cursor-pointer items-center text-xs text-text-accent'
  112. href={
  113. locale === LanguagesSupported[1]
  114. ? 'https://docs.dify.ai/v/zh-hans/guides/knowledge-base/integrate-knowledge-within-application'
  115. : 'https://docs.dify.ai/guides/knowledge-base/integrate-knowledge-within-application'
  116. }
  117. target='_blank' rel='noopener noreferrer'
  118. >
  119. <RiBookOpenLine className='mr-1 text-text-accent' />
  120. {t('common.datasetMenus.viewDoc')}
  121. </a>
  122. </div>
  123. }
  124. >
  125. <div className='system-xs-medium-uppercase inline-flex cursor-pointer items-center space-x-1 text-text-secondary'>
  126. <span>{t('common.datasetMenus.noRelatedApp')}</span>
  127. <RiInformation2Line className='h-4 w-4' />
  128. </div>
  129. </Tooltip>
  130. )}
  131. </div>
  132. }
  133. const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
  134. const {
  135. children,
  136. params: { datasetId },
  137. } = props
  138. const pathname = usePathname()
  139. const hideSideBar = /documents\/create$/.test(pathname)
  140. const { t } = useTranslation()
  141. const { isCurrentWorkspaceDatasetOperator } = useAppContext()
  142. const media = useBreakpoints()
  143. const isMobile = media === MediaType.mobile
  144. const { data: datasetRes, error, mutate: mutateDatasetRes } = useSWR({
  145. url: 'fetchDatasetDetail',
  146. datasetId,
  147. }, apiParams => fetchDatasetDetail(apiParams.datasetId))
  148. const { isCreate, isEdit, isOperation } = GetDatasetAuth(datasetRes)
  149. const { data: relatedApps } = useSWR({
  150. action: 'fetchDatasetRelatedApps',
  151. datasetId,
  152. }, apiParams => fetchDatasetRelatedApps(apiParams.datasetId))
  153. const navigation = useMemo(() => {
  154. const baseNavigation = [
  155. // { name: 'api & webhook', href: `/datasets/${datasetId}/api`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
  156. // { name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
  157. ]
  158. if (isEdit) {
  159. baseNavigation.unshift({
  160. name: t('common.datasetMenus.hitTesting'),
  161. href: `/datasets/${datasetId}/hitTesting`,
  162. icon: TargetIcon,
  163. selectedIcon: TargetSolidIcon,
  164. })
  165. baseNavigation.push({
  166. name: t('common.datasetMenus.settings'),
  167. href: `/datasets/${datasetId}/settings`,
  168. icon: Cog8ToothIcon,
  169. selectedIcon: Cog8ToothSolidIcon,
  170. })
  171. }
  172. if (datasetRes?.provider !== 'external') {
  173. baseNavigation.unshift({
  174. name: t('common.datasetMenus.documents'),
  175. href: `/datasets/${datasetId}/documents`,
  176. icon: DocumentTextIcon,
  177. selectedIcon: DocumentTextSolidIcon,
  178. })
  179. }
  180. return baseNavigation
  181. }, [datasetRes?.provider, datasetId, t])
  182. useEffect(() => {
  183. if (datasetRes)
  184. document.title = `${datasetRes.name || 'Dataset'} - Dify`
  185. }, [datasetRes])
  186. const setAppSiderbarExpand = useStore(state => state.setAppSiderbarExpand)
  187. useEffect(() => {
  188. const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
  189. const mode = isMobile ? 'collapse' : 'expand'
  190. setAppSiderbarExpand(isMobile ? mode : localeMode)
  191. }, [isMobile, setAppSiderbarExpand])
  192. if (!datasetRes && !error)
  193. return <Loading type='app' />
  194. return (
  195. <div className='flex grow overflow-hidden'>
  196. {!hideSideBar && <AppSideBar
  197. title={datasetRes?.name || '--'}
  198. icon={datasetRes?.icon || 'https://static.dify.ai/images/dataset-default-icon.png'}
  199. icon_background={datasetRes?.icon_background || '#F5F5F5'}
  200. desc={datasetRes?.description || '--'}
  201. isExternal={datasetRes?.provider === 'external'}
  202. navigation={navigation}
  203. extraInfo={!isCurrentWorkspaceDatasetOperator ? mode => <ExtraInfo isMobile={mode === 'collapse'} relatedApps={relatedApps} expand={mode === 'collapse'} /> : undefined}
  204. iconType={datasetRes?.data_source_type === DataSourceType.NOTION ? 'notion' : 'dataset'}
  205. />}
  206. <DatasetDetailContext.Provider value={{
  207. indexingTechnique: datasetRes?.indexing_technique,
  208. dataset: datasetRes,
  209. mutateDatasetRes: () => mutateDatasetRes(),
  210. }}>
  211. <div className="grow overflow-hidden bg-background-default-subtle">{children}</div>
  212. </DatasetDetailContext.Provider>
  213. </div>
  214. )
  215. }
  216. export default React.memo(DatasetDetailLayout)